package org.ribizli.basicgameserver;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.ribizli.basicgameserver.data.TopList;
import org.ribizli.basicgameserver.data.User;

public class AmoebaGame extends Game {
	
	static final Logger logger = LogManager.getLogger(AmoebaGame.class);

  private Hashtable<String, User> users;
  private Hashtable<String, Field> userOnField;
  private Hashtable<String, ArrayList<JSONObject>> userMessages;
  private String matchRequest = null;

  public static final String FIELD_ACTION = "action";
  public static final String FIELD_USERNAME = "username";
  public static final String FIELD_SCORE = "score";
  public static final String FIELD_X = "x";
  public static final String FIELD_Y = "y";
  public static final String FIELD_FIRST = "first";
  public static final String FIELD_OPPONENT = "opponent";
  public static final String FIELD_WINNER_ROW = "winner_row";
  public static final String FIELD_ONLINE_USERS = "online_users";
  public static final String FIELD_PLAYING_USERS = "playing_users";
  public static final String FIELD_MESSAGE_TEXT = "message_text";

  public static final String ACTION_NEW_GAME = "NEW_GAME";
  public static final String ACTION_REMATCH = "REMATCH";
  public static final String ACTION_WAITING_FOR_OPPONENT = "WAITING_FOR_OPPONENT";
  public static final String ACTION_YOUR_TURN = "YOUR_TURN";
  public static final String ACTION_MAKE_MOVE = "MAKE_MOVE";
  public static final String ACTION_INVALID_MOVE = "INVALID_MOVE";
  public static final String ACTION_YOU_WON = "YOU_WON";
  public static final String ACTION_YOU_LOOSE = "YOU_LOOSE";
  public static final String ACTION_OPPONENT_LEFT = "OPPONENT_LEFT";
  public static final String ACTION_USER_STATISTICS = "USER_STATISTICS";
  public static final String ACTION_SEND_MESSAGE = "SEND_MESSAGE";
  public static final String ACTION_UPDATE_PROFILE = "UPDATE_PROFILE";

  public static final String ACTION_INVALID_STATE = "INVALID_STATE";

  public static final String ANY_OPPONENT = "*";

  public void init() {
    super.init();

    users = new Hashtable<String, User>();
    userOnField = new Hashtable<String, Field>();
    userMessages = new Hashtable<String, ArrayList<JSONObject>>();
  }

  @Override
  public String getID() {
    return AmoebaGame.class.getCanonicalName();
  }

  @Override
  public JSONObject messages(String token, JSONObject messages)
      throws GameException {
    JSONObject json = new JSONObject();
    if (!users.containsKey(token))
      throw new GameException(ERROR_USER_NOT_LOGGED);
    JSONArray responseMessages = new JSONArray();
    try {
      json.put(BasicGameServer.FIELD_MESSAGES, responseMessages);
    } catch (JSONException e) {
      logger.log(Level.ERROR, "JSONException", e);
    }
    try {
      JSONArray requestMessages = messages
          .getJSONArray(BasicGameServer.FIELD_MESSAGES);
      int length = requestMessages.length();
      for (int i = 0; i < length; i++) {
        logger.debug("Message - token: " + token + ", message: " + requestMessages.getJSONObject(i));
        processMessage(token, requestMessages.getJSONObject(i));
      }

      for (JSONObject o : userMessages.get(token)) {
        responseMessages.put(o);
      }
      if (userMessages.get(token).size() > 0) {
      	logger.debug("Response - token: " + token + ", messages: " + responseMessages);
      }
      userMessages.get(token).clear();
    } catch (JSONException e) {
      logger.log(Level.ERROR, "JSONException", e);
    }
    return json;
  }

  private void processMessage(String token, JSONObject message)
      throws JSONException {
    String action = message.getString(FIELD_ACTION);
    if (ACTION_NEW_GAME.equals(action)) {
      actionNewGame(token, message);
    } else if (ACTION_REMATCH.equals(action)) {
      actionRematch(token, message);
    } else if (ACTION_MAKE_MOVE.equals(action)) {
      actionMakeMove(token, message);
    } else if (ACTION_USER_STATISTICS.equals(action)) {
      int onlineUsers = users.size();
      int playingUsers = userOnField.size();
      userMessages.get(token).add(
          new JSONObject().put(FIELD_ACTION, ACTION_USER_STATISTICS).put(
              FIELD_ONLINE_USERS, onlineUsers).put(FIELD_PLAYING_USERS,
              playingUsers));
    } else if (ACTION_SEND_MESSAGE.equals(action)) {
      Field f = userOnField.get(token);
      String text = message.getString(FIELD_MESSAGE_TEXT);
      if (f != null && !"".equals(text)) {
        userMessages.get(f.getOpponent(token)).add(
            new JSONObject().put(FIELD_ACTION, ACTION_SEND_MESSAGE).put(
                FIELD_OPPONENT, users.get(token).getUsername()).put(
                FIELD_MESSAGE_TEXT, text));
      }
    } else if (ACTION_UPDATE_PROFILE.equals(action)) {
    	update(token, message.optString(BasicGameServer.FIELD_PASSWORD), message.optJSONObject(BasicGameServer.FIELD_DETAILS));
    	userMessages.get(token).add(new JSONObject().put(FIELD_ACTION, ACTION_UPDATE_PROFILE));
    }
  }

  private void actionRematch(String token, JSONObject message)
      throws JSONException {
    Field f = userOnField.get(token);
    if (f == null) {
      logger.warn("User not on field, but requested a rematch");
      userMessages.get(token).add(
          new JSONObject().put(FIELD_ACTION, ACTION_INVALID_STATE));
      return;
    }
    String opponent = f.getOpponent(token);
    if (f.isFinished()) {
      f.rematch(token);
      userMessages.get(opponent).add(
          new JSONObject().put(FIELD_ACTION, ACTION_REMATCH));
    } else {
      logger.warn("Game not ended, but the user requested a rematch");
      userMessages.get(token).add(
          new JSONObject().put(FIELD_ACTION, ACTION_INVALID_STATE));
      return;
    }
    if (f.canRematch()) {
      f.start();
      userMessages.get(opponent).add(getNewGameMessage(token, true));
      userMessages.get(token).add(getNewGameMessage(opponent, false));
    }
  }

  private void actionMakeMove(String token, JSONObject message)
      throws JSONException {
    int x = message.getInt(FIELD_X), y = message.getInt(FIELD_Y);
    Field f = userOnField.get(token);
    if (f == null) {
      logger.warn("User not on the field but tyies to make a move.");
      return;
    }
    boolean won = false;
    try {
      won = f.putMark(token, x, y);
    } catch (Exception e) {
      userMessages.get(token).add(
          new JSONObject().put(FIELD_ACTION, ACTION_INVALID_MOVE));
    }
    String opp = f.getOpponent(token);
    if (!won) {
      userMessages.get(opp).add(getYourTurnMessage(x, y));
    } else {
      // scoring
      User p = users.get(token);
      User o = users.get(opp);
      p.hasBeaten(o);
      // token won
      logger.debug("Scoring: " + p.getUsername() + " - " + p.getScore() + ", " + 
          o.getUsername() + " - " + o.getScore());
      JSONArray winner = getWinnerRow(f);
      userMessages.get(token).add(
          getYouWonMessage(winner, new int[] { p.getScore(), o.getScore() }));
      userMessages.get(opp).add(
          getYouLooseMessage(x, y, winner, new int[] { o.getScore(),
              p.getScore() }));
    }
  }

  private synchronized void actionNewGame(String token, JSONObject message)
      throws JSONException {
    Field currField = userOnField.remove(token);
    if (currField != null) {
      // user already on field
      String opponent = currField.getOpponent(token);
      // retire from the Field
      logger.debug("Retire from field: " + token + ", " + opponent);
      userOnField.remove(opponent);
      userMessages.get(opponent).add(getOpponentLeftMessage());
    }

    if (matchRequest != null && matchRequest.equals(token)) {
      // user already registered for play
      logger.warn("User alread registered for a play: " + token);
      userMessages.get(token).add(getWiatingForOpponentMessage());
      return;
    }
    if (matchRequest != null) {
      String opponent = matchRequest;
      matchRequest = null;
      Field f = new Field(opponent, token);
      userOnField.put(token, f);
      userOnField.put(opponent, f);
      userMessages.get(opponent).add(getNewGameMessage(token, true));
      userMessages.get(token).add(getNewGameMessage(opponent, false));
      logger.debug("Second player ready: " + token);
    } else {
      matchRequest = token;
      userMessages.get(token).add(getWiatingForOpponentMessage());
      logger.debug("First player ready: " + token);
    }
  }

  private JSONObject getWiatingForOpponentMessage() throws JSONException {
    return new JSONObject().put(FIELD_ACTION, ACTION_WAITING_FOR_OPPONENT);
  }

  private JSONObject getYouLooseMessage(int x, int y, JSONArray winner,
      int[] score) throws JSONException {
    return new JSONObject().put(FIELD_ACTION, ACTION_YOU_LOOSE).put(FIELD_X, x)
        .put(FIELD_Y, y).put(FIELD_WINNER_ROW, winner).put(FIELD_SCORE,
            new JSONArray(score));
  }

  private JSONObject getYouWonMessage(JSONArray winner, int[] score)
      throws JSONException {
    return new JSONObject().put(FIELD_ACTION, ACTION_YOU_WON).put(
        FIELD_WINNER_ROW, winner).put(FIELD_SCORE, new JSONArray(score));
  }

  private JSONArray getWinnerRow(Field f) throws JSONException {
    int[][] w = f.getWinner();
    JSONArray row = new JSONArray();
    for (int i = 0; i < w.length; i++) {
      row.put(new JSONArray().put(w[i][0]).put(w[i][1]));
    }
    return row;
  }

  private JSONObject getYourTurnMessage(int x, int y) throws JSONException {
    return new JSONObject().put(FIELD_ACTION, ACTION_YOUR_TURN).put(FIELD_X, x)
        .put(FIELD_Y, y);
  }

  private JSONObject getNewGameMessage(String token, boolean first)
      throws JSONException {
    User u = users.get(token);
    return new JSONObject().put(FIELD_ACTION, ACTION_NEW_GAME).put(
        FIELD_OPPONENT, users.get(token).getUsername()).put(FIELD_FIRST, first)
        .put(FIELD_SCORE, u.getScore().intValue());
  }

  @Override
  public String newPlayer(String username, String password, JSONObject info)
      throws GameException {
    String token = null;
    User u = null;
    // if the user is already logged in, only return the previous token
    for (String t : users.keySet()) {
      User usr = users.get(t);
      if (usr.getUsername().equals(username)) {
        token = t;
        u = usr;
        break;
      }
    }

    if (u == null) {
      u = User.findUser(username);
      if (u == null) {
        throw new GameException(ERROR_USER_NOT_REGISTERED);
      }
    }
    if (!u.checkPassword(password)) {
      throw new GameException(ERROR_WRONG_PASSWORD);
    }

    // The user is not logged in
    if (token == null) {
      token = getRandomToken();
      synchronized (this) {
        users.put(token, u);
        userMessages.put(token, new ArrayList<JSONObject>());
      }
    } else {
    	/* If the user was on a field */
    	Field f = userOnField.remove(token);
    	if (f != null) {
        String opp = f.getOpponent(token);
        userOnField.remove(opp);
        // and the game was not finished yet
        logger.info("User relogined and he was in an active game: " + token);
        try {
          userMessages.get(opp).add(getOpponentLeftMessage());
        } catch (JSONException e) {
        }
    	}
    	userMessages.get(token).clear();
    }
    if (info != null) {
      try {
        info.put(FIELD_USERNAME, u.getUsername()).put(FIELD_SCORE,
            u.getScore().intValue());
      } catch (JSONException e) {
      }
    }
    return token;
  }

  @Override
  public synchronized void playerLeft(String token) {
    users.remove(token);
    userMessages.remove(token);
    Field f = userOnField.remove(token);
    if (f != null) {
      String opp = f.getOpponent(token);
      userOnField.remove(opp);
      // and the game was not finished yet
      logger.info("User disconnected from an active game: " + token);
      try {
        userMessages.get(opp).add(getOpponentLeftMessage());
      } catch (JSONException e) {
      }

    }
    if (matchRequest != null && matchRequest.equals(token)) {
      logger.info("User disconnected from the waiting queue: " + token);
      matchRequest = null;
    }
  }

  private JSONObject getOpponentLeftMessage() throws JSONException {
    return new JSONObject().put(FIELD_ACTION, ACTION_OPPONENT_LEFT);
  }

  @Override
  public void register(String username, String password,
      Map<String, String> details) throws GameException {
    User u = User.findUser(username);
    if (u != null) {
      throw new GameException(ERROR_USER_ALREADY_REGISTERED);
    }
    u = new User();
    u.setUsername(username);
    u.setPassword(User.SHA1(password));
    u.setDetails(details);
    logger.info("Registring: " + username + ", pass: " + password);
    try {
      u.save();
    } catch (HibernateException e) {
      logger.log(Level.ERROR, e.getMessage(), e);
      throw new GameException(e.getMessage());
    }
  }

  @SuppressWarnings("unchecked")
	private void update(String token, String password, JSONObject details) {
    User u = users.get(token);
    if (password != null && !"".equals(password)) {
      u.setPassword(User.SHA1(password));
    }
    if (details != null && details.length() > 0) {
      Map<String, String> uMap = u.getDetails();
      for (Iterator<String> iterator = details.keys(); iterator.hasNext();) {
				String key = iterator.next();
				uMap.put(key, details.optString(key));
			}
    }
    u.save();
  }

	@Override
	public JSONObject getTopScores(String user) throws GameException {
		TopList top = new TopList(user);
		try {
			return top.getTopScores();
		} catch (JSONException e) {
			throw new GameException(BasicGameServer.ERROR_INVALID_MESSAGE);
		}
	}
}
